/**--------------File Info--------------------------------------------------------------                                 
** Descriptions:            读MQ-9数据并显示     
** Created by:              Li_moji
** Created date:            2021/11/7
** Version:                 1.0       
**---------------------------------------------------------------------------------------**/
#include<reg52.h>
#include<intrins.h>

#define uchar unsigned char
#define uint unsigned int

#define PCF8591ADDR 0x90  //PCF8591地址字，末位0读1写

#define I2cRead 1         //I2C读写方向
#define I2cWrite 0        //I2C读写方向
#define at24c02addr 0xA0  //器件地址，由接线决定


sbit beep = P2^3;//蜂鸣器
sbit Dout = P2^0;//SDA数模转换数字输出
sbit Din = P1^0;//传感器数字输出，超过设定值为0
sbit inter = P3^3;//外部中断1触发引脚
sbit SDA = P2^0;//I2C数据线
sbit SCL = P2^1;//I2C时钟线
bit AckFlag;//应答标志

sbit LCDEN = P3^4;
sbit LCDRS = P3^5;//寄存器选择，0为指令寄存器，1为数据寄存器
sbit LCDRW = P3^6;//读写信号线，0为写，1为读

uchar title[] = {"CO-potency"};//要显示的固定内容（第一行）
uchar table[] = {"        ppm"};//要显示的数据，空出来，到时候填充

uint datAddr;
uchar dat;

/**                              
Descriptions:定时器中断0初始化函数                 
Parameter:
Return:
**/
void timer0Init()
{
    EA = 1;//打开总中断
    ET0 = 1;//打开定时器中断0
    TR0 = 1;//启动定时器0
    TMOD |= 0x01;//定时器工作模式1，16位定时模式
    TH0 = 0xED;
    TL0 = 0xFF;//定时5ms
}

/**                               
Descriptions: 外部中断1初始化函数                
Parameter:
Return:   
**/
void int1Init()
{
    EA = 1;//打开总中断
    EX1 = 1;//打开外部中断1
    IT1 = 1;//下降沿触发
}

/**                              
Descriptions:延时n毫秒                 
Parameter:int n
Return:   
**/
void delay(uint n) 
{
    uint i=0,j=0;
    for(i=0;i<n;i++)
        for(j=0;j<123;j++);
}
/**
Descriptions:  延时5us               
Parameter:
Return:   
**/
void delay5us()
{
    _nop_();
}

/**                                
Descriptions:I2C通信起始函数 ,SCL置高，SDA下降沿即为起始，但两线每种电平都要保持5us以上才有效               
Parameter:
Return:   
**/
void I2cStart()
{
    SCL = 1;
    SDA = 1;
    delay5us();
    SDA = 0;
    delay5us();
}

/**
Descriptions: I2C停止函数                
Parameter:
Return:   
**/
void I2cStop()
{
    SDA = 0;
    SCL = 1;
    delay5us();
    SDA = 1;
    delay5us();
}

/**
Descriptions:读SDA的从机应答状态                 
Parameter:
Return:bit 0应答，1不应答   
**/
bit readACK()
{
    SCL = 1;
    delay5us();
    if(SDA){
        SCL = 0;//拉低
        return 1;
    }else{
        SCL = 0;
        return 0;
    }    
}

/**
Descriptions:主机发送应答                 
Parameter:bit 1非应答 0应答
Return:   
**/
void sendACK(bit answerCode)
{
    if(answerCode){
        SDA = 1;
    }else{
        SDA = 0;
    }
    SCL = 1;
    delay5us();
    SCL = 0;//用完拉低时钟
    SDA= 1;//释放数据总线
}


/**
Descriptions:总线发送一个字节到从站                 
Parameter:uchar dat一个字的数据
Return:   
**/
void I2cSendByte(uchar dat)
{
    uchar i;
    
    for(i = 0;i < 8;i ++){
        SCL = 0;//时钟为低才允许数据变化
        if(dat & 0x80)//判断最高位为1？
        {
            SDA = 1;//发1
        }else
        {
            SDA = 0;//发0
        }
        SCL = 1;//1位1位的发，所以一开一关
        dat <<= 1;//dat左移一位，次高位变为最高位，下次循环继续判断最高位
    }
    SCL = 0;
    SDA = 1;//释放数据总线
}

/**
Descriptions:主站从从站读一个字节                 
Parameter:addr从机地址
Return:   
**/
uchar I2cReadByte(){
    uchar count,dat;
    for(count = 0; count < 8; count ++){
        dat <<= 1;
        //上次dat得到的一位数据左移一位，为下一位读取准备，放在后面第八位读进来时会顶掉最开始读进来一位
        SCL = 0;//允许数据变化，从机操作SDA
        SCL = 1;//置高才将移出的一位放在SDA上
        if(SDA)
            dat |= 0x01;//1|=1=1；dat得到一位数据
    }
    return dat;
}




/**
Descriptions:发送数据到从站                 
Parameter:uchar addr写入的首地址
Return:   
**/
void at24c02write(uchar addr,dat)
{
    I2cStart();//起始
    I2cSendByte(at24c02addr + I2cWrite);//发送 从机地址 + 读写方向
    if(readACK()){//读应答函数：1为不应答，0为应答
        AckFlag = 1;
    }else{
        AckFlag = 0;
    }
    I2cSendByte(addr);//从机的首地址
    if(readACK())
        AckFlag = 1;
    else
        AckFlag = 0;
    I2cSendByte(dat);//发送一个字节
    if(readACK())
        AckFlag = 1;
    else
        AckFlag = 0;

} 



/**
Descriptions:主站从从站读数据 ，中途由写改变为读的方式（只读的方式与此不同）：               
Parameter:addr从机数据的首地址
Return:dat返回的一个字节   
**/
void at24c02read_2(uchar addr){
    uchar dat;

    I2cStart();
    I2cSendByte(at24c02addr + I2cWrite);//发送从机地址和读写方向
    if(readACK())
        AckFlag = 1;
    else
        AckFlag = 0;
    I2cSendByte(addr);//发送从机数据的首地址
    readACK();//不管是不是应答了
    I2cStart();//重新开始
    dat = I2cReadByte();//读一字节
    sendACK(1);//非应答
    I2cStop();
}

/**
Descriptions:主站从从站读数据               
Parameter:addr从机数据的首地址
Return:dat返回的一个字节   
**/
unsigned char at24c02read_1(uchar addr){
    uchar dat;

    I2cStart();                         //开始
    I2cSendByte(at24c02addr + I2cWrite);//发送从站地址和写方向
    if(readACK())                      //读从站应答
        AckFlag = 1;
    else
        AckFlag = 0;                   //应答
    I2cSendByte(addr);                 //发送读的首地址
    if(readACK())
        AckFlag = 1;
    else    
        AckFlag = 0;                   //读从站应答
    I2cSendByte(at24c02addr + I2cRead);//发送从站地址和读方向
    if(readACK())                       //读从站应答
        AckFlag = 1;
    else
        AckFlag = 0;
    
    dat = I2cReadByte();                //读从站一个字节数据
    sendACK(1);                         //给从站回复非应答信号
        
    I2cStop();                          //停止
    
    return dat;                         //把读到的数据返回出来
}


/**
Descriptions:判断LCD是否忙               
Parameter:
Return:   
**/
void read_lcd_busy(){
    uchar busy;

    P0 = 0xff;  //1111 1111,本开发板P0为数据口
    LCDRS = 0;  //
    LCDRW = 1;  //读写1为读

    do{
        LCDEN = 1;//使能开
        busy = P0;//P0口状态忙不忙存出来
        LCDEN = 0;//关使能
    }while(0x80 & busy);//1000 0000 判断忙不忙
}

/**
Descriptions:LCD写控制指令               
Parameter:指令码
Return:   
**/
void write_cmd(uchar cmd){
    read_lcd_busy();//先读忙不忙
    LCDRS = 0;//命令寄存器
    LCDRW = 0;//写命令

    P0 = cmd;

    delay(5);
    LCDEN = 1;
    LCDEN = 0;
}

/**
Descriptions:LCD写数据               
Parameter:数据
Return:   
**/
void write_dat(uchar dat){
    read_busy();
    LCDRS = 1;//数据寄存器
    LCDRW = 0;//写
    
    P0 = dat;

    delay(5);
    LCDEN = 1;
    LCDEN = 0;
}

/**
Descriptions:LCD初始化                 
Parameter:
Return:   
**/
void lcd1602Init(){
  write_cmd(0x38);//16*2，5*7点阵，8位数据接口
  write_cmd(0x0c);//显示器开，光标关闭
  write_cmd(0x06);//文字不动，地址自加
  write_cmd(0x01);//清屏
}

/**
Descriptions:LCD显示               
Parameter:数据数组指针，显示地址指针(1602每个格子都有地址，对应不同位置)
Return:   
**/
void display(uchar *title, uchar *table){
    lcd1602Init();//先初始化lcd1602
  
    write_cmd(0x80);//第一行第一列的地址

    //从上述位置连着显示9个格子,预设内容显示
    
    for(datAddr = 0; datAddr < 9;datAddr ++){
        write_dat(*title ++);
    }
    write_dat(0x3A);//好像是显示一个冒号

    //把要显示的数据填充进准备好的空数组里
    for(datAddr = 0; datAddr <8; datAddr ++){
        uchar bitTiQuQi = 0x80;//1000 0000
        table[datAddr] = dat & bitTiQuQi;
        bitTiQuQi >>1;//0100 0000以此类推
    }

    for(datAddr = 0; datAddr < 11;datAddr ++){    //8位数据连着ppm一起11位
        write_dat(*table ++);//11位显示出来
    }
}

//********************************MAIN************************************//
void main()
{
    timer0Init();
    int1Init();//调用初始化中断

    beep = 1;//先排除下开机干扰之类的，免得开机报警
    while(1)
    {
        if(Din == 0)
        delay(100);//防止干扰，延时后再次确认
        if(Din == 0)
        {
        inter = 1;//下降沿触发外部中断1
        delay(5);
        inter = 0;
        }
        
        dat = at24c02read_1(PCF8591ADDR);//读数据
        
        display(&title,&table);//显示数据
    }
}


/**                               
Descriptions:中断1服务程序，且写在main后也不用在前面声明
Parameter:
Return:   
**/
void int1() interrupt 2
{
    beep = 0;
}  
